package rad.framework.richfaces;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.faces.model.SelectItem;
import javax.persistence.Query;

import rad.framework.jpa.QueryUtil;
import rad.framework.jsf.JsfUtil;
import rad.framework.jsf.ListCache;
import rad.framework.seam.BaseReferenceQuery;

import org.apache.commons.lang.StringUtils;
import org.jboss.seam.core.Expressions.ValueExpression;

public class Picker<COMP, TGT, SRC> extends BaseReferenceQuery<COMP, TGT, SRC> {

	public Picker(Object component, String name) {
		super(component, name);
	}

	public Picker(Object component, String instanceName, String name) {
		super(component, instanceName, name);
	}

	// --------------------------------------

	public boolean isRenderFilter() {
		return isRenderPaging() || !StringUtils.isEmpty(getFilter());
	}

	public boolean isRenderPaging() {
		return getPageCount() > 1;
	}

	// --------------------------------------
	// static vs dynamic ? filter, page count, etc

	private String filter;

	public String getFilter() {
		return filter;
	}

	public void setFilter(String filter) {
		if (this.filter == null || filter.length() == 0
				|| !this.filter.equals(filter)) {
			clear();
		}
		this.filter = filter;
	}

	public String getWildcardFilter() {
		return getWildcardFilter(filter);
	}

	public boolean isFiltered() {
		return !StringUtils.isEmpty(getFilter());
	}

	// --------------------------------------

	private Long pageCount = new Long(1);

	public Long getPageCount() {
		return pageCount;
	}

	public void setPageCount(Long pageCount) {
		this.pageCount = pageCount;
	}

	public void calculatePageCount(Long count) {
		long size = getPageSize();
		long mod = count % size;
		setPageCount(new Double(Math.floor(count / size) + (mod > 0 ? 1 : 0))
				.longValue());
		if (getPageCount() == 0) {
			setPageCount(1l); // TODO not sure about this
		}
		if (getPage() > getPageCount()) {
			setPage(1);
		}
	}

	public int getPageSize() {
		return 20;
	}

	// --------------------------------------

	private int page = 1;

	public int getPage() {
		return page;
	}

	public void setPage(int page) {
		if (this.page != page) {
			clear();
		}
		this.page = page;
	}

	// --------------------------------------

	public int getFirstResult() {
		if (getPage() > 0) {
			return (getPage() - 1) * getPageSize();
		} else {
			return 0;
		}
	}

	// --------------------------------------

	protected List<SelectItem> available;

	public void setAvailable(List<SelectItem> available) {
		this.available = available;
	}

	// --------------------------------------

	protected List<SelectItem> selected;

	public void setSelected(List<SelectItem> selected) {
		this.selected = selected;
	}

	public List<SelectItem> getAvailable() {
		ListCache cache = new ListCache() {
			protected Object proceed(Object ctx) {
				initialize();
				return available;
			}
		};
		return (List<SelectItem>) cache.invoke(null, getName() + "Available");
	}

	// --------------------------------------

	public List<SelectItem> getSelected() {
		ListCache cache = new ListCache() {
			protected Object proceed(Object ctx) {
				initialize();
				return selected;
			}
		};
		return (List<SelectItem>) cache.invoke(null, getName() + "Selected");
	}

	// --------------------------------------

	public void initialize() {
		initSelected();
		initAvailable();
	}

	public void clear() {
		this.available = null;
		this.selected = null;
	}

	// --------------------------------------

	protected void initAvailable() {
		String eql = getEql();

		Query qry = getEntityManager().createQuery(eql);
		Query cqry = getEntityManager().createQuery(
				QueryUtil.getCountEjbql(eql, getTargetEntityName(), "t1"));

		if (isFiltered()) {
			String param = getFilterParamName();
			String filter = getWildcardFilter();
			qry.setParameter(param, filter);
			cqry.setParameter(param, filter);
		}

		calculatePageCount((Long) cqry.getSingleResult());

		qry.setFirstResult(getFirstResult());
		qry.setMaxResults(getPageSize());
		setAvailable(format(qry.getResultList(), true));
	}

	// --------------------------------------

	protected void initSelected() {
		Collection list = getCollection();
		setSelected(sort(format(list, false)));
	}

	private ValueExpression<Object> association;

	protected Collection getCollection() {
		if (association == null) {
			association = getExpression("#{" + getInstanceName() + "."
					+ getName() + "}");
		}
		// TODO permissions ??
		return (Collection) association.getValue();
	}

	// --------------------------------------

	protected List<SelectItem> format(Collection list, Boolean fromQuery) {
		// TODO refactor to base ???
		ValueExpression valueExpression = getExpression(getItemValue());
		ValueExpression labelExpression = getExpression(getItemLabel());
		List<SelectItem> items = new ArrayList<SelectItem>(list.size());
		for (Object object : list) {
			Object temp = null;
			if (isDistinct() && fromQuery) {
				temp = ((Object[]) object)[0];
			} else {
				temp = object;
			}
			JsfUtil.getRequestMap().put(getAlias(), temp);
			Object label = labelExpression.getValue();
			if (label == null) {
				label = "";
			}
			items.add(new SelectItem(valueExpression.getValue(), label
					.toString()));
			// TODO disabled el ???
			JsfUtil.getRequestMap().remove(getAlias());
		}
		return items;
	}

	// --------------------------------------

	private Object[] toAdd;

	public Object[] getToAdd() {
		return toAdd;
	}

	public void setToAdd(Object[] toAdd) {
		this.toAdd = toAdd;
	}

	private Object[] toRemove;

	public Object[] getToRemove() {
		return toRemove;
	}

	public void setToRemove(Object[] toRemove) {
		this.toRemove = toRemove;
	}

	public String getConverterId() {
		return "javax.faces.Number";
	}

	// --------------------------------------

	public void add() {
		doAdd();
		flush();
		clear();
	}

	public void remove() {
		doRemove();
		flush();
		clear();
	}

	public void doAdd() {
		for (Object id : getToAdd()) {
			TGT ref = (TGT) getEntityManager().find(getTargetEntityClass(), id);
			add(getInstance(), ref);
			if (getFolder() != null) {
				getFolder().addChild(ref);
			}
		}
	}

	public void doRemove() {
		for (Object id : getToRemove()) {
			TGT ref = (TGT) getEntityManager().find(getTargetEntityClass(), id);
			remove(getInstance(), ref);
			if (getFolder() != null) {
				getFolder().removeChild2(ref);
			}
		}
	}

	protected void add(SRC instance, TGT ref) {
	}

	protected void remove(SRC instance, TGT ref) {
	}

	// --------------------------------------

	protected int addExtension(StringBuilder builder, int count) {
		// m2m
		if (isManyToMany() && isManaged()) {
			// restrict to current instance
			count = addConjunction(builder, count);
			builder.append(getAlias2());
			builder.append(" = #{");
			builder.append(getInstanceName());
			builder.append("}");

			// not already associated to current instance
			count = addConjunction(builder, count);
			builder.append(getAlias());
			builder.append(" not in elements(");
			builder.append(getAlias2());
			builder.append(".");
			builder.append(getName());
			builder.append(")");

			// not the current instance for association to self
			if (getTargetEntityClass().equals(getSourceEntityClass())) {
				count = addConjunction(builder, count);
				builder.append(getAlias());
				builder.append(" != ");
				builder.append(getAlias2());
			}
		} else if (isManyToMany() && !isManaged() && selected.size() > 0) {
			count = addConjunction(builder, count);
			builder.append(getAlias());
			builder.append(".id");
			builder.append(" not in (");

			int i = 0;
			for (SelectItem item : selected) {
				if (i++ > 0) {
					builder.append(",");
				}
				builder.append((Long) item.getValue());
			}
			builder.append(")");
		}

		// o2m shared or o2o
		// not already associated
		if (isOneToManyShared() || isOneToOne()) {
			count = addConjunction(builder, count);
			builder.append(getAlias());
			builder.append(".");
			builder.append(getInverseName());
			builder.append(" is null");
		}
		return count;
	}

	// --------------------------------------

	public Picker addRestriction(String restriction) {
		_addRestriction(restriction);
		return this;
	}

	// --------------------------------------

	protected String getAlias2() {
		return "t2";
	}

	protected String getItemValue() {
		return "#{t1.id}";
	}

	protected String getFrom2() {
		if (isManyToMany() && isManaged()) {
			return ", " + getSourceEntityName() + " " + getAlias2();
		} else {
			return "";
		}
	}

	protected String getInverseName() {
		return null;
	}

	protected Class getSourceEntityClass() {
		return getInstance().getClass();
		// return ReflectionUtil.getParameterizedType(this.getClass(), 1);
	}

	// --------------------------------------

	protected boolean isOneToManyShared() {
		if (getFolder() != null) {
			return getFolder().isOneToManyShared();
		}
		return false;
	}

	protected boolean isOneToOne() {
		if (getFolder() != null) {
			return getFolder().isOneToOne();
		}
		return false;
	}

	protected boolean isManyToMany() {
		if (getFolder() != null) {
			return getFolder().isManyToMany();
		}
		return !isOneToManyShared() && !isOneToOne();
	}

	// --------------------------------------

	private ValueExpression managed;

	protected boolean isManaged() {
		// TODO is this still needed as pickers are only present on managed
		// editors
		if (managed == null) {
			managed = getExpression("#{" + getComponentName() + ".managed}");
		}
		Boolean value = (Boolean) managed.getValue();
		if (value == null) {
			return false;
		} else {
			return value.booleanValue();
		}
	}

	// --------------------------------------

	private FolderTreeNode folder;

	public FolderTreeNode getFolder() {
		return folder;
	}

	public void setFolder(FolderTreeNode folder) {
		this.folder = folder;
	}
}
